package com.yibs.shiro;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yibs.common.lang.Result;
import com.yibs.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author liwh
 * @title: JwtFilter
 * @projectName ifs-manager
 * @description: 这里我们继承的是Shiro内置的AuthenticatingFilter，一个内置了可以自动登录方法的的过滤器，
 * 有些同学继承BasicHttpAuthenticationFilter也是可以的
 *
 *     anon（匿名）  org.apache.shiro.web.filter.authc.AnonymousFilter
 *     authc（身份验证）      org.apache.shiro.web.filter.authc.FormAuthenticationFilter
 *     authcBasic（http基本验证）   org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
 *     logout（退出）       org.apache.shiro.web.filter.authc.LogoutFilter
 *     noSessionCreation（不创建session） org.apache.shiro.web.filter.session.NoSessionCreationFilter
 *     perms(许可验证) org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
 *     port（端口验证） org.apache.shiro.web.filter.authz.PortFilter
 *     rest  (rest方面) org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
 *     roles（权限验证） org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
 *     ssl （ssl方面） org.apache.shiro.web.filter.authz.SslFilter
 *     user （用户方面） org.apache.shiro.web.filter.authc.UserFilter
 *
 * 我们需要重写几个方法：
 *
 * createToken：实现登录，我们需要生成我们自定义支持的JwtToken
 * onAccessDenied：拦截校验，当头部没有Authorization时候，我们直接通过，不需要自动登录；当带有的时候，首先我们校验jwt的有效性，没问题我们就直接执行executeLogin方法实现自动登录
 * onLoginFailure：登录异常时候进入的方法，我们直接把异常信息封装然后抛出
 * preHandle：拦截器的前置拦截，因为我们是前后端分析项目，项目中除了需要跨域全局配置之外，我们再拦截器中也需要提供跨域支持。这样，拦截器才不会在进入Controller之前就被限制了。
 *
 * @date 2020/7/2817:18
 */
public class JwtFilter extends AuthenticatingFilter {
    @Resource
    private JwtUtils jwtUtils;

    /**
     * 应用程序构建了一个终端用户认证信息的AuthenticationToken实例后，再去调用Subject.login方法(委托给realm处理method:doGetAuthenticationInfo)。
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)) {
            return null;
        }

        return new JwtToken(jwt);
    }

    /**
     * FormAuthenticationFiltershiro提供的登录的filter，如果用户未登录，即AuthenticatingFilter中的isAccessAllowed判断了用户未登录，
     * 则会调用onAccessDenied方法做用户登录操作。
     * 若用户请求的不是登录地址，则跳转到登录地址，并且返回false直接终止filter链。
     * 若用户请求的是登录地址，若果是post请求则进行登录操作，由AuthenticatingFilter中提供的executeLogin方法执行。
     * 否则直接通过继续执行filter链，并最终跳转到登录页面（因为用户请求的就是登录地址，若不是登录地址也会重定向到登录地址）.
     * onAccessDenied：表示当访问拒绝时是否已经处理了；如果返回true表示需要继续处理；如果返回false表示该拦截器实例已经处理了，将直接返回即可
     *
     * org.apache.shiro.web.filter.AccessControlFilter类里面的onPreHandle方法：
     * isAccessAllowed：判断是否登录,在登录的情况下会走此方法，此方法返回true直接访问控制器
     * onAccessDenied：是否是拒绝登录，没有登录的情况下会走此方法
     * 如果isAccessAllowed方法返回True，则不会再调用onAccessDenied方法，如果isAccessAllowed方法返回Flase,则会继续调用onAccessDenied方法。
     * 而onAccessDenied方法里面则是具体执行登陆的地方。
     * 由于我们已经登陆，所以此方法就会返回True(filter放行),所以上面的onPreHandle方法里面的onAccessDenied方法就不会被执行
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)) {
            return true;
        } else {

            // 校验jwt
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                throw new ExpiredCredentialsException("token已失效，请重新登录");
            }

            /*
            执行登录，委托给realm处理method:doGetAuthenticationInfo
            登录成功：onLoginSuccess
            登录失败：onLoginFailure，如果用户不存在或者账号状态异常，出现异常的情况，因为前后端分离，重写方法返回json
             */
            return executeLogin(servletRequest, servletResponse);
        }
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result result = Result.fail(throwable.getMessage());
        String json = JSONUtil.toJsonStr(result);

        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {

        }
        return false;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求，这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }

        return super.preHandle(request, response);
    }
}
